package com.tianji.aigc.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.aigc.config.SystemPromptConfig;
import com.tianji.aigc.config.ToolResultHolder;
import com.tianji.aigc.constants.Constant;
import com.tianji.aigc.enums.ChatEventTypeEnum;
import com.tianji.aigc.service.ChatService;
import com.tianji.aigc.service.ChatSessionService;
import com.tianji.aigc.vo.ChatEventVO;
import com.tianji.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.vectorstore.QuestionAnswerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.Map;



/**
 * 与数据量无交互，不用继承 ServiceImpl<ChatSessionMapper, ChatSession> 接口
 */
@Slf4j
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "tj.ai",name = "chat-type",havingValue = "ENHANCE")
public class ChatServiceImpl implements ChatService {

    //使用redis来存储生成的标识容器信息
    private final StringRedisTemplate stringRedisTemplate;
    private static final String GENERATE_STATUS_KEY = "GENERATE_STATUS"; //Redis数据库中存储的key
    // 输出结束的标记
    private static final ChatEventVO STOP_EVENT = ChatEventVO.builder().eventType(ChatEventTypeEnum.STOP.getValue()).build();
    private final ChatClient chatClient;
    private final ChatClient openAiChatClient;
    private final SystemPromptConfig systemPromptConfig; //系统提示词的配置 在nacos中的配置文件中
    private final ChatMemory chatMemory;//解决联调 停止会话后，数据不保存至数据库的问题
    // 存储大模型的生成状态，这里采用ConcurrentHashMap是确保线程安全
    // 目前的版本暂时用Map实现，如果考虑分布式环境的话，可以考虑用redis来实现
   // private static final Map<String, Boolean> GENERATE_STATUS = new ConcurrentHashMap<>();
    //向量库
    private final VectorStore vectorStore;

    private final ChatSessionService chatSessionService;

 /*  public String systemMessage() {
        return this.systemPromptConfig.getRouteAgentSystemMessage().get();
    }*/



    //封装响应格式与内容后返回结果  Flux<ChatEventVO>  流式响应，不阻塞，JSON格式 的数据能使结果样式更符合预期
    @Override
    public Flux<ChatEventVO> chat(String question, String sessionId) {
        // 获取对话id
        var conversationId = ChatService.getConversationId(sessionId);
        // 大模型输出内容的缓存器，用于在输出中断后的数据存储
        var outputBuilder = new StringBuilder();
        // 生成请求id
        var requestId = IdUtil.simpleUUID();
        //获取用户id
        var userId = UserContext.getUser();

        // 异步更新会话信息  用户所提问题为对话标题
      //  this.chatSessionService.update(sessionId, question, userId);

        // 创建RAG增强  相似度 0.6 取前6个结果
      var qaAdvisor = QuestionAnswerAdvisor.builder(this.vectorStore)
                .searchRequest(SearchRequest.builder().similarityThreshold(0.6d).topK(6).build())
                .build();

        //用Hash结构的redis
        var hashOps = this.stringRedisTemplate.boundHashOps(GENERATE_STATUS_KEY);
        return this.chatClient.prompt()
                .system(promptSystem ->
                    promptSystem.text(this.systemPromptConfig.getChatSystemMessage().get())  //得到配置文件中的系统提示词，并设置
                            .param("now", DateUtil.now())  //设置当前的时间参数
                )
                .advisors(advisor -> advisor
                        // 设置RAG增强
                        .advisors(qaAdvisor)
                        .param(ChatMemory.CONVERSATION_ID, conversationId))
                .toolContext(Map.of(Constant.REQUEST_ID,requestId,Constant.USER_ID,userId))  //通过工具上下文传递参数 requestID  userId
                .user(question)
                .stream()
                .chatResponse()
                .doFirst(()->hashOps.put(sessionId,"true"))  // 生成开始时，设置标识  redis方案
                .doOnError(throwable->hashOps.delete(sessionId)) // 异常结束时，删除标识
                .doOnComplete(()->hashOps.delete(sessionId)) // 正常结束时，删除标识
                .doOnCancel(() -> {
                    // 当输出被取消时，保存输出的内容到历史记录中
                    this.saveStopHistoryRecord(conversationId, outputBuilder.toString());
                })
                .doOnComplete(()->this.updateHistoryTitle(sessionId,outputBuilder.toString(),userId))
                //.doFinally(updateHistoryTitle(sessionId,outputBuilder.toString(),userId))
                .takeWhile(response-> hashOps.get(sessionId) !=null)  //后续生成的条件，true：继续生成，false：停止生成 redis中有无sessionId对应的value
                .map(chatResponse -> {
                    // 获取大模型的输出的内容
                    String text = chatResponse.getResult().getOutput().getText();
                    // 追加到输出内容中
                    outputBuilder.append(text);
                    // 对于响应结果进行处理，如果是最后一条数据，就把此次消息id放到内存中
                    // 主要用于存储消息数据到 redis中，可以根据消息di获取的请求id，再通过请求id就可以获取到参数列表了
                    // 从而解决，在历史聊天记录中没有参数的问题
                    var stopReason = chatResponse.getResult().getMetadata().getFinishReason();
                    if(StrUtil.equals(Constant.STOP,stopReason)){
                        String messageId = chatResponse.getMetadata().getId();//获取消息id
                        ToolResultHolder.put(messageId,Constant.REQUEST_ID,requestId); //将消息id与请求id关联 可在全局使用
                    }
                    // 封装响应对象
                    return ChatEventVO.builder()
                            .eventData(text)
                            .eventType(ChatEventTypeEnum.DATA.getValue())
                            .build();
                })
                .concatWith(Flux.defer(()->{
                            // 通过请求id获取到参数列表，如果不为空，就将其追加到返回结果中
                                    var result = ToolResultHolder.get(requestId);
                                    if(ObjectUtil.isNotEmpty(result)){
                                        //清空参数列表
                                        ToolResultHolder.remove(requestId);
                                        // 响应给前端的参数数据
                                        var chatEventVO = ChatEventVO.builder()
                                                .eventData(result)
                                                .eventType(ChatEventTypeEnum.PARAM.getValue())
                                                .build();
                                        return Flux.just(chatEventVO,STOP_EVENT);
                                    }
                                   return Flux.just(STOP_EVENT);
                                }));
    }


    /**
     * 保存停止输出的记录
     *
     * @param conversationId 会话id
     * @param content        大模型输出的内容
     */
    private void saveStopHistoryRecord(String conversationId, String content) {
        this.chatMemory.add(conversationId, new AssistantMessage(content));
    }

    @Override
    public void stop(String sessionId) {
       /* // 移除标记
        GENERATE_STATUS.remove(sessionId);*/
        var hashOps = this.stringRedisTemplate.boundHashOps(GENERATE_STATUS_KEY); //仍然绑定原key
        hashOps.delete(sessionId);
    }

    //由AI大模型根据会话内容生成标题
    private void updateHistoryTitle(String sessionId, String context,Long userId){

        if(ObjectUtil.isEmpty(context)){
            return ;
        }
      //  String conversationId = ChatService.getConversationId(sessionId);
        String prompt =this.systemPromptConfig.getGenerateTitleSystemMessage().get();

        String title = chatClient.prompt(prompt)
                .user(context)
                .call().content();
        chatSessionService.updateTitleByAI(sessionId,title,userId);

    }

    @Override
    public String chatText(String question) {
        return this.openAiChatClient.prompt()
                .system(promptSystem -> promptSystem.text(this.systemPromptConfig.getTextSystemMessage().get()))
                .user(question)
                .call()
                .content();
    }
}
